package main

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"slices"
	"strings"
	"sync"

	"github.com/spf13/cobra"
)

type GoType struct {
	Name    string `json:"name"`
	PkgPath string `json:"pkgPath"`
}

type TableField struct {
	Name         string  `json:"name"`
	ColumnName   string  `json:"columnName"`
	DbType       string  `json:"dbType"`
	GoType       *GoType `json:"goType"`
	Comment      string  `json:"comment"`
	DefaultValue bool    `json:"defaultValue"`
	PrimaryKey   bool    `json:"primaryKey"`
	AutoInc      bool    `json:"autoInc"`
	Unique       bool    `json:"unique"`
	Nullable     bool    `json:"nullable"`
	OnUpdate     bool    `json:"onUpdate"`
	SoftDelete   bool    `json:"softDelete"`
	BaseModel    bool    `json:"baseModel"`
}

type TableDesc struct {
	Name      string        `json:"name"`
	TableName string        `json:"tableName"`
	Comment   string        `json:"comment"`
	BaseType  *GoType       `json:"baseType"`
	Fields    []*TableField `json:"fields"`
}

type CommonOptions struct {
	writeGo bool
	force   bool
	gofmt   bool
}

func (opts *CommonOptions) AddFlags(cmd *cobra.Command) {
	cmd.Flags().BoolVarP(&opts.writeGo, "write", "w", false, "write to {table name}.go")
	cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "force write")
	cmd.Flags().BoolVarP(&opts.gofmt, "gofmt", "F", false, "run gofmt for {table name}.go")
}

func runGofmt(gofile string) error {
	cmdArgs := slices.Clone(goFmtCmd)
	for i, a := range cmdArgs {
		if a == "{}" {
			cmdArgs[i] = gofile
		}
	}
	gofmt := exec.Command(cmdArgs[0], cmdArgs[1:]...)
	gofmt.Stderr = os.Stderr
	return gofmt.Run()
}

func writeOutput(outputFile string, templateName string, doc any, opts *CommonOptions) error {
	err := func() error {
		var w io.Writer
		if opts.writeGo {
			fi, _ := os.Stat(outputFile)
			if fi != nil && !opts.force {
				fatalf("file %s existed\n", outputFile)
			}
			f, err := os.Create(outputFile)
			if err != nil {
				return err
			}
			defer f.Close()
			w = f
		} else {
			w = os.Stdout
		}
		return rootTemplate.ExecuteTemplate(w, templateName, doc)
	}()
	if err != nil {
		return err
	}

	if opts.writeGo && opts.gofmt {
		_ = runGofmt(outputFile)
	}

	return nil
}

var (
	columnTypes map[string]string

	mysqlTypeMap = map[string]string{
		"int":                "int64",
		"integer":            "int64",
		"tinyint":            "int64",
		"smallint":           "int64",
		"mediumint":          "int64",
		"bigint":             "int64",
		"int unsigned":       "uint64",
		"integer unsigned":   "uint64",
		"tinyint unsigned":   "uint64",
		"smallint unsigned":  "uint64",
		"mediumint unsigned": "uint64",
		"bigint unsigned":    "uint64",
		"bit":                "byte",
		"bool":               "bool",
		"enum":               "string",
		"set":                "string",
		"varchar":            "string",
		"char":               "string",
		"tinytext":           "string",
		"mediumtext":         "string",
		"text":               "string",
		"longtext":           "string",
		"blob":               "[]byte",
		"tinyblob":           "[]byte",
		"mediumblob":         "[]byte",
		"longblob":           "[]byte",
		"date":               "time.Time",
		"datetime":           "time.Time",
		"timestamp":          "time.Time",
		"time":               "time.Time",
		"float":              "float64",
		"double":             "float64",
		"decimal":            "float64",
		"binary":             "[]byte",
		"varbinary":          "[]byte",
		"json":               "[]byte",
	}

	tablePrefixes []string

	baseModelType    *GoType
	baseModelColumns map[string]bool
	softDeleteColumn string
	softDeleteType   *GoType

	goFmtCmd = []string{"gofmt", "-w", "{}"}
)

var initCommonConfig = sync.OnceFunc(func() {
	commonConfig := getAppConfig().Common
	tablePrefixes = commonConfig.TablePrefixes
	columnTypes = commonConfig.ColumnTypes
	if commonConfig.GoFmt != nil {
		goFmtCmd = append([]string{commonConfig.GoFmt.Cmd}, commonConfig.GoFmt.Args...)
	}
	modelConfig := getAppConfig().Model
	if modelConfig.BaseModel != nil && modelConfig.BaseModel.TypeName != "" {
		baseModelType = parseGoType(modelConfig.BaseModel.TypeName)
		baseModelColumns = make(map[string]bool)
		for _, name := range modelConfig.BaseModel.Fields {
			baseModelColumns[name] = true
		}
	}
	if modelConfig.SoftDelete != nil {
		softDeleteColumn = modelConfig.SoftDelete.ColumnName
		softDeleteType = parseGoType(modelConfig.SoftDelete.TypeName)
	}
})

func mapSqlTypeToGo(tableName string, column *TableColumn) (string, error) {
	goType := columnTypes[tableName+"."+column.columnName]
	if goType != "" {
		return goType, nil
	}
	goType = mysqlTypeMap[column.dataType]
	if goType == "" {
		return "", fmt.Errorf("cannot map sql data type %s", column.dataType)
	}
	if column.isNullable {
		nullable := mysqlTypeMap[column.dataType+"?"]
		if nullable != "" {
			goType = nullable
		} else {
			goType = "*" + goType
		}
	}
	return goType, nil
}

func parseGoType(typeExpr string) *GoType {
	// {prefix}{pkgPath}/{pkgName}.{typname}
	s := strings.TrimLeft(typeExpr, "*[]")
	prefix := len(typeExpr) - len(s)

	slash := strings.LastIndexByte(s, '/')
	quadId := s[slash+1:]
	dot := strings.LastIndexByte(quadId, '.')
	pkg := ""
	if dot >= 0 {
		pkg = s[:slash+1+dot]
	}
	return &GoType{
		PkgPath: pkg,
		Name:    typeExpr[:prefix] + quadId,
	}
}

func getTableDesc(database string, tableName string, useBaseModel bool) (*TableDesc, error) {
	tableColumns, err := getTableColumns(database, tableName)
	if err != nil {
		return nil, err
	}

	if len(tableColumns) == 0 {
		return nil, fmt.Errorf("table %q not found", database+"."+tableName)
	}

	tableComment, err := getTableComment(database, tableName)
	if err != nil {
		return nil, err
	}

	structName := camelCase(removePrefix(tablePrefixes, tableName), false)

	fields := make([]*TableField, 0, len(tableColumns))

	var baseType *GoType
	if len(baseModelColumns) > 0 {
		baseModelColumnNum := 0
		for _, column := range tableColumns {
			if baseModelColumns[column.columnName] {
				baseModelColumnNum++
			}
		}
		if baseModelColumnNum == len(baseModelColumns) {
			baseType = baseModelType
		}
	}

	for _, column := range tableColumns {
		if useBaseModel && baseType != nil && baseModelColumns[column.columnName] {
			// skip base model fields
			continue
		}

		fieldName := camelCase(column.columnName, false)
		var goType *GoType
		if column.columnName == softDeleteColumn {
			goType = softDeleteType
		} else {
			goTypeName, err := mapSqlTypeToGo(tableName, column)
			if err != nil {
				return nil, err
			}
			goType = parseGoType(goTypeName)
		}
		fields = append(fields, &TableField{
			Name:         fieldName,
			ColumnName:   column.columnName,
			DbType:       column.dataType,
			GoType:       goType,
			Comment:      column.columnComment,
			PrimaryKey:   column.isPrimaryKey,
			DefaultValue: column.columnDefault.Valid || column.hasAutoIncrement,
			AutoInc:      column.hasAutoIncrement,
			Unique:       column.isUniqueKey,
			Nullable:     column.isNullable,
			OnUpdate:     column.hasOnUpdate,
			SoftDelete:   column.columnName == softDeleteColumn,
			BaseModel:    baseType != nil && baseModelColumns[column.columnName],
		})
	}

	if !useBaseModel {
		baseType = nil
	}
	return &TableDesc{
		Name:      structName,
		TableName: tableName,
		Comment:   tableComment,
		Fields:    fields,
		BaseType:  baseType,
	}, nil
}

func splitDatabaseTable(tableName string) (string, string, string) {
	db, tbl, ok := strings.Cut(tableName, ".")
	if !ok {
		return getAppConfig().Mysql.Database, tableName, ""
	}
	return db, tbl, db + "."
}
